/**
 * Sparse rss
 *
 * Copyright (c) 2010-2013 Stefan Handschuh
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

package de.shandschuh.sparserss.handler;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.preference.PreferenceManager;
import de.shandschuh.sparserss.Strings;
import de.shandschuh.sparserss.provider.FeedData;
import de.shandschuh.sparserss.provider.FeedDataContentProvider;
import de.shandschuh.sparserss.service.FetcherService;

public class RSSHandler extends DefaultHandler {
	private static final String TAG_RSS = "rss";
	
	private static final String TAG_RDF = "rdf";
	
	private static final String TAG_FEED = "feed";
	
	private static final String TAG_ENTRY = "entry";
	
	private static final String TAG_ITEM = "item";
	
	private static final String TAG_UPDATED = "updated";
	
	private static final String TAG_TITLE = "title";
	
	private static final String TAG_LINK = "link";
	
	private static final String TAG_DESCRIPTION = "description";
	
	private static final String TAG_MEDIA_DESCRIPTION = "media:description";
	
	private static final String TAG_CONTENT = "content";
	
	private static final String TAG_MEDIA_CONTENT = "media:content";
	
	private static final String TAG_ENCODEDCONTENT = "encoded";
	
	private static final String TAG_SUMMARY = "summary";
	
	private static final String TAG_PUBDATE = "pubDate";
	
	private static final String TAG_DATE = "date";
	
	private static final String TAG_LASTBUILDDATE = "lastBuildDate";
	
	private static final String TAG_ENCLOSURE = "enclosure";
	
	private static final String TAG_GUID = "guid";
	
	private static final String TAG_AUTHOR = "author";
	
	private static final String TAG_NAME = "name";
	
	private static final String TAG_CREATOR = "creator";
	
	private static final String TAG_ENCLOSUREURL = "url";
	
	private static final String TAG_ENCLOSURETYPE = "type";
	
	private static final String TAG_ENCLOSURELENGTH = "length";
	
	private static final String ATTRIBUTE_URL = "url";
	
	private static final String ATTRIBUTE_HREF = "href";
	
	private static final String ATTRIBUTE_TYPE = "type";
	
	private static final String ATTRIBUTE_LENGTH = "length";
	
	private static final String ATTRIBUTE_REL = "rel";
	
	private static final String UTF8 = "UTF-8";
	
	private static final String PERCENT = "%";
	
	/** This can be any valid filename character sequence which does not contain '%' */
	private static final String PERCENT_REPLACE = "____";
	
	private static final String[] TIMEZONES = {"MEST", "EST", "PST"};
	
	private static final String[] TIMEZONES_REPLACE = {"+0200", "-0500", "-0800"};

	private static final int TIMEZONES_COUNT = 3;

	private static final Pattern unicodePattern = Pattern.compile("&#x?[[a-f][0-9]]{2,};", Pattern.CASE_INSENSITIVE);
	
	private static long KEEP_TIME = 345600000l; // 4 days
	
	
	private static final DateFormat[] PUBDATE_DATEFORMATS = {
		new SimpleDateFormat("EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US),
		new SimpleDateFormat("d' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US),
		new SimpleDateFormat("EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' 'z", Locale.US),
	};

	private static final DateFormat[] PUBDATE_DATEONLYFORMATS = {
		new SimpleDateFormat("EEE', 'd' 'MMM' 'yyyy", Locale.US),
		new SimpleDateFormat("d' 'MMM' 'yyyy", Locale.US),
		new SimpleDateFormat("EEE', 'd' 'MMM' 'yyyy", Locale.US),
	};

	private static final DateFormat[] UPDATE_DATEFORMATS = {
		new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"),
		new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz", Locale.US),
		new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US),
	};
	
	private static final DateFormat[] UPDATE_DATEONLYFORMATS = {
		new SimpleDateFormat("yyyy-MM-dd"),
	};
	
	private static final String Z = "Z";
	
	private static final String GMT = "GMT";
	
	private static final StringBuilder DB_FAVORITE  = new StringBuilder(Strings.DB_AND).append("(").append(Strings.DB_EXCLUDEFAVORITE).append(')');

	private static final Pattern imgPattern = Pattern.compile("<img src=\\s*['\"]([^'\"]+)['\"][^>]*>", Pattern.CASE_INSENSITIVE); // middle () is group 1; s* is important for non-whitespaces; ' also usable
	
	private Context context;
	
	private Date lastUpdateDate;
	
	String id;

	private boolean titleTagEntered;
	
	private boolean updatedTagEntered;
	
	private boolean linkTagEntered;
	
	private boolean descriptionTagEntered;
	
	private boolean pubDateTagEntered;
	
	private boolean dateTagEntered;
	
	private boolean lastUpdateDateTagEntered;
	
	private boolean guidTagEntered;
	
	private StringBuilder title;
	
	private StringBuilder dateStringBuilder;
	
	private Date entryDate;

	private Date entryDateOnly;
	
	private StringBuilder entryLink;
	
	private StringBuilder description;
	
	private StringBuilder enclosure;
	
	private Uri feedEntriesUri;
	
	private int newCount;
	
	private boolean feedRefreshed;
	
	private boolean firstEntry;
	
	private boolean feedHasDate;
	
	private String feedTitle;
	
	private String feedBaseUrl;
	
	private boolean done;
	
	private Date keepDateBorder;
	
	private InputStream inputStream;
	
	private Reader reader;

	private boolean fetchImages;
	
	private boolean cancelled;
	
	private Date lastBuildDate;
	
	private Date lastBuildDateOnly;
	
	private long realLastUpdate;
	
	private long now;
	
	private StringBuilder guid;
	
	private boolean efficientFeedParsing;
	
	private boolean authorTagEntered;
	
	private StringBuilder author;
	
	private boolean nameTagEntered;
	
	private boolean enclosureTagEntered;
	
	private boolean enclosureUrlTagEntered;
	
	private boolean enclosureTypeTagEntered;
	
	private boolean enclosureLengthTagEntered;
	
	private StringBuilder enclosureUrl;
	
	private StringBuilder enclosureType;
	
	private StringBuilder enclosureLength;
	
	public RSSHandler(Context context) {
		KEEP_TIME = Long.parseLong(PreferenceManager.getDefaultSharedPreferences(context).getString(Strings.SETTINGS_KEEPTIME, "4"))*86400000l;
		this.context = context;
		this.efficientFeedParsing = true;
	}
	
	public void init(Date lastUpdateDate, final String id, String title, String url) {
		now = System.currentTimeMillis();
		final long keepDateBorderTime = KEEP_TIME > 0 ? now-KEEP_TIME : 0;
		
		keepDateBorder = new Date(keepDateBorderTime);
		this.lastUpdateDate = lastUpdateDate;
		this.id = id;
		feedEntriesUri = FeedData.EntryColumns.CONTENT_URI(id);
		
		final String query = new StringBuilder(FeedData.EntryColumns.DATE).append('<').append(keepDateBorderTime).append(DB_FAVORITE).toString();
		
		FeedData.deletePicturesOfFeed(context, feedEntriesUri, query);
		
		context.getContentResolver().delete(feedEntriesUri, query, null);
		newCount = 0;
		feedRefreshed = false;
		firstEntry = true;
		feedHasDate = false;
		feedTitle = title;
		initFeedBaseUrl(url);
		this.title = null;
		this.dateStringBuilder = null;
		this.entryLink = null;
		this.description = null;
		this.enclosure = null;
		inputStream = null;
		reader = null;
		entryDate = null;
		lastBuildDate = null;
		realLastUpdate = lastUpdateDate.getTime();
		
		done = false;
		cancelled = false;
		
		titleTagEntered = false;
		updatedTagEntered = false;
		linkTagEntered = false;
		descriptionTagEntered = false;
		pubDateTagEntered = false;
		dateTagEntered = false;
		lastUpdateDateTagEntered = false;
		guid = null;
		guidTagEntered = false;
		authorTagEntered = false;
		author = null;
		enclosureTagEntered = false;
		enclosureUrlTagEntered = false;
		enclosureUrl = null;
		enclosureTypeTagEntered = false;
		enclosureType = null;
		enclosureLengthTagEntered = false;
		enclosureLength = null;
	}
	
	public void initFeedBaseUrl(String url) {
		int index = url.indexOf('/', 8); // this also covers https://
		
		if (index > -1) {
			feedBaseUrl = url.substring(0, index);
		} else {
			feedBaseUrl = null;
		}
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
		if (TAG_UPDATED.equals(localName)) {
			updatedTagEntered = true;
			dateStringBuilder = new StringBuilder();
		} else if (TAG_ENTRY.equals(localName) || TAG_ITEM.equals(localName)) {
			description = null;
			entryLink = null;
			if (!feedRefreshed) {
				ContentValues values = new ContentValues();
				
				if (feedTitle == null && title != null && title.length() > 0) {
					values.put(FeedData.FeedColumns.NAME, title.toString().trim());
				}
				values.put(FeedData.FeedColumns.ERROR, (String) null);
				values.put(FeedData.FeedColumns.LASTUPDATE, now - 1000);
				long realLastUpdateFromFeed = 0;
				if (lastBuildDate != null) {
					realLastUpdateFromFeed = lastBuildDate.getTime();
					feedHasDate = true;
				} else if (lastBuildDateOnly != null) {
					realLastUpdateFromFeed = lastBuildDateOnly.getTime();
					feedHasDate = true;
				}
				if (entryDate != null) {
					if (entryDate.getTime() > realLastUpdateFromFeed) {
						realLastUpdateFromFeed = entryDate.getTime();
						feedHasDate = true;
					}
				} else if (entryDateOnly != null) {
					if (entryDateOnly.getTime() > realLastUpdateFromFeed) {
						realLastUpdateFromFeed = entryDateOnly.getTime();
						feedHasDate = true;
					}
				}
				if (!feedHasDate) {
					realLastUpdateFromFeed = now - 1000;
				}
				realLastUpdate = Math.max(realLastUpdateFromFeed, realLastUpdate);
				setRealLastUpdateDate(values);
				context.getContentResolver().update(FeedData.FeedColumns.CONTENT_URI(id), values, null, null);
				title = null;
				feedRefreshed = true;
			}
		} else if (TAG_TITLE.equals(localName)) {
			if (title == null) {
				titleTagEntered = true;
				title = new StringBuilder();
			}
		} else if (TAG_LINK.equals(localName)) {
			if (authorTagEntered) {
				return;
			}
			if (TAG_ENCLOSURE.equals(attributes.getValue(Strings.EMPTY, ATTRIBUTE_REL))) {
				startEnclosure(attributes, attributes.getValue(Strings.EMPTY, ATTRIBUTE_HREF));
			} else if (entryLink == null || qName.equals(localName)) {
				// this indicates either there is no link yet or it is a non prefix tag which is preferred
				entryLink = new StringBuilder();
				
				boolean foundLink = false;
				
				for (int n = 0, i = attributes.getLength(); n < i; n++) {
					if (ATTRIBUTE_HREF.equals(attributes.getLocalName(n))) {
						if (attributes.getValue(n) != null) {
							entryLink.append(attributes.getValue(n));
							foundLink = true;
							linkTagEntered = false;
						} else {
							linkTagEntered = true;
						}
						break;
					}
				}
				if (!foundLink) {
					linkTagEntered = true;
				}
			}
		} else if ((TAG_DESCRIPTION.equals(localName) && !TAG_MEDIA_DESCRIPTION.equals(qName)) || (TAG_CONTENT.equals(localName) && !TAG_MEDIA_CONTENT.equals(qName))) {
			descriptionTagEntered = true;
			description = new StringBuilder();
		} else if (TAG_SUMMARY.equals(localName)) {
			if (description == null) {
				descriptionTagEntered = true;
				description = new StringBuilder();
			}
		} else if (TAG_PUBDATE.equals(localName)) {
			pubDateTagEntered = true;
			dateStringBuilder = new StringBuilder();
		} else if (TAG_DATE.equals(localName)) {
			dateTagEntered = true;
			dateStringBuilder = new StringBuilder();
		} else if (TAG_LASTBUILDDATE.equals(localName)) {
			lastUpdateDateTagEntered = true;
			dateStringBuilder = new StringBuilder();
		} else if (TAG_ENCODEDCONTENT.equals(localName)) {
			descriptionTagEntered = true;
			description = new StringBuilder();
		} else if (TAG_ENCLOSURE.equals(localName)) {
			enclosureTagEntered = true;
			startEnclosure(attributes, attributes.getValue(Strings.EMPTY, ATTRIBUTE_URL));
		} else if (TAG_GUID.equals(localName)) {
			guidTagEntered = true;
			guid = new StringBuilder();
		} else if (TAG_AUTHOR.equals(localName) || TAG_CREATOR.equals(localName)) {
			authorTagEntered = true;
			if (TAG_CREATOR.equals(localName)) {
				nameTagEntered = true; // we simulate the existence of a name tag to trigger the characters(..) method
			}
			if (author == null) {
				author = new StringBuilder();
			} else if (author.length() > 0){
				// this indicates multiple authors
				author.append(Strings.COMMASPACE);
			}
		} else if (TAG_NAME.equals(localName)) {
			nameTagEntered = true;
		} else if (enclosureTagEntered) {
			if (TAG_ENCLOSUREURL.equals(localName)) {
				enclosureUrlTagEntered = true;
				enclosureUrl = new StringBuilder();
			} else if (TAG_ENCLOSURETYPE.equals(localName)) {
				enclosureTypeTagEntered = true;
				enclosureType = new StringBuilder();
			} else if (TAG_ENCLOSURELENGTH.equals(localName)) {
				enclosureLengthTagEntered = true;
				enclosureLength = new StringBuilder();
			}
		}
	}
	
	private void startEnclosure(Attributes attributes, String url) {
		if (enclosure == null && url != null && url.length() > 0) { // fetch the first enclosure only
			enclosure = new StringBuilder(url);
			enclosure.append(Strings.ENCLOSURE_SEPARATOR);
			
			String value = attributes.getValue(Strings.EMPTY, ATTRIBUTE_TYPE);
			
			if (value != null) {
				enclosure.append(value);
			}
			enclosure.append(Strings.ENCLOSURE_SEPARATOR);
			value = attributes.getValue(Strings.EMPTY, ATTRIBUTE_LENGTH);
			if (value != null) {
				enclosure.append(value);
			}
		}
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException {
		if (titleTagEntered) {
			title.append(ch, start, length);
		} else if (updatedTagEntered) {
			dateStringBuilder.append(ch, start, length);
		} else if (linkTagEntered) {
			entryLink.append(ch, start, length);
		} else if (descriptionTagEntered) {
			description.append(ch, start, length);
		} else if (pubDateTagEntered) {
			dateStringBuilder.append(ch, start, length);
		} else if (dateTagEntered) {
			dateStringBuilder.append(ch, start, length);
		} else if (lastUpdateDateTagEntered) {
			dateStringBuilder.append(ch, start, length);
		} else if (guidTagEntered) {
			guid.append(ch, start, length);
		} else if (authorTagEntered && nameTagEntered) {
			author.append(ch, start, length);
		} else if (enclosureUrlTagEntered) {
			enclosureUrl.append(ch, start, length);
		} else if (enclosureTypeTagEntered) {
			enclosureType.append(ch, start, length);
		} else if (enclosureLengthTagEntered) {
			enclosureLength.append(ch, start, length);
		}
	}
	
	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException {
		if (TAG_TITLE.equals(localName)) {
			titleTagEntered = false;
		} else if ((TAG_DESCRIPTION.equals(localName) && !TAG_MEDIA_DESCRIPTION.equals(qName)) || TAG_SUMMARY.equals(localName) || (TAG_CONTENT.equals(localName) && !TAG_MEDIA_CONTENT.equals(qName)) || TAG_ENCODEDCONTENT.equals(localName)) {
			descriptionTagEntered = false;
		} else if (TAG_LINK.equals(localName)) {
			linkTagEntered = false;
		} else if (TAG_UPDATED.equals(localName)) {
			entryDate = parseUpdateDate(dateStringBuilder.toString());
			entryDateOnly = parseUpdateDateOnly(dateStringBuilder.toString());
			updatedTagEntered = false;
		} else if (TAG_PUBDATE.equals(localName)) {
			entryDate = parsePubdateDate(dateStringBuilder.toString().replace(Strings.TWOSPACE, Strings.SPACE));
			entryDateOnly = parsePubdateDateOnly(dateStringBuilder.toString().replace(Strings.TWOSPACE, Strings.SPACE));
			pubDateTagEntered = false;
		} else if (TAG_LASTBUILDDATE.equals(localName)) {
			lastBuildDate = parsePubdateDate(dateStringBuilder.toString().replace(Strings.TWOSPACE, Strings.SPACE));
			lastBuildDateOnly = parsePubdateDateOnly(dateStringBuilder.toString().replace(Strings.TWOSPACE, Strings.SPACE));
			lastUpdateDateTagEntered = false;
		} else if (TAG_DATE.equals(localName)) {
			entryDate = parseUpdateDate(dateStringBuilder.toString());
			entryDateOnly = parseUpdateDateOnly(dateStringBuilder.toString());
			dateTagEntered = false;
		} else if (TAG_ENTRY.equals(localName) || TAG_ITEM.equals(localName)) {
			boolean dateOnly;
			if ((entryDate == null) && (entryDateOnly != null)) {
				entryDate = entryDateOnly;
				dateOnly = true;
			} else {
				dateOnly = false;
			}
			if (firstEntry) {
				firstEntry = false;
				if (!feedHasDate && (entryDate != null)) {
					realLastUpdate = entryDate.getTime();
					ContentValues values = new ContentValues();
					setRealLastUpdateDate(values);
					context.getContentResolver().update(FeedData.FeedColumns.CONTENT_URI(id), values, null, null);
				}
				if (dateOnly) {
					Calendar calendar = Calendar.getInstance();
					calendar.setTime(lastUpdateDate);
					calendar.set(Calendar.AM_PM, Calendar.AM);
					calendar.set(Calendar.HOUR, 0);
					calendar.set(Calendar.MINUTE, 0);
					calendar.set(Calendar.SECOND, 0);
					calendar.set(Calendar.MILLISECOND, 0);
					lastUpdateDate = calendar.getTime();
				}
			}
			if (title != null && (entryDate == null || ((entryDate.after(lastUpdateDate) || (dateOnly && entryDate.equals(lastUpdateDate)) || !efficientFeedParsing) && entryDate.after(keepDateBorder)))) {
				ContentValues values = new ContentValues();
				
				if (entryDate != null && entryDate.getTime() > realLastUpdate) {
					realLastUpdate = entryDate.getTime();
					
					setRealLastUpdateDate(values);
					context.getContentResolver().update(FeedData.FeedColumns.CONTENT_URI(id), values, null, null);
					values.clear();
				}
				
                boolean entryDateIsLastUpdateDate = (dateOnly && (lastUpdateDate.equals(entryDate)));
                boolean entryIsNewerThanLastUpdate = (entryDate != null) && entryDate.after(lastUpdateDate);
				int dateType = FeedData.DATETYPE_DEFAULT;
				if (entryDate != null) {
					if (dateOnly) {
						dateType = FeedData.DATETYPE_ONLYDATE;
					}
					values.put(FeedData.EntryColumns.DATE, entryDate.getTime());
					values.putNull(FeedData.EntryColumns.READDATE);
				}
				values.put(FeedData.EntryColumns.DATETYPE, dateType);
				String titleString = unescapeString(title.toString().trim());
				values.put(FeedData.EntryColumns.TITLE, titleString);
				
				if (author != null) {
					values.put(FeedData.EntryColumns.AUTHOR, unescapeString(author.toString()));
				}
				
				Vector<String> images = null;
				
				if (description != null) {
					String descriptionString = description.toString().trim().replaceAll(Strings.HTML_SPAN_REGEX, Strings.EMPTY);
					
					if (descriptionString.length() > 0) {
						if (fetchImages) {
							images = new Vector<String>(4);
							
							Matcher matcher = imgPattern.matcher(description);
							
							while (matcher.find()) {
								String match = matcher.group(1).replace(Strings.SPACE, Strings.URL_SPACE);
								
								images.add(match);
								try {
									// replace the '%' that may occur while urlencode such that the img-src url (in the abstract text) does reinterpret the parameters
									descriptionString = descriptionString.replace(match, new StringBuilder(Strings.FILEURL).append(FeedDataContentProvider.IMAGEFOLDER).append(Strings.IMAGEID_REPLACEMENT).append(URLEncoder.encode(match.substring(match.lastIndexOf('/')+1), UTF8)).toString().replace(PERCENT, PERCENT_REPLACE));
								} catch (UnsupportedEncodingException e) {
									// UTF-8 should be supported
								}
							}
						}
						values.put(FeedData.EntryColumns.ABSTRACT, descriptionString);
					}
				}
				
				String enclosureString = null;
				
				StringBuilder existenceStringBuilder = new StringBuilder(FeedData.EntryColumns.LINK).append(Strings.DB_ARG);
				
				if (enclosure == null && enclosureUrl != null && enclosureUrl.length() > 0) {
					enclosure = enclosureUrl;
					enclosure.append(Strings.ENCLOSURE_SEPARATOR);
					if (enclosureType != null && enclosureType.length() > 0) {
						enclosure.append(enclosureType);
					}
					enclosure.append(Strings.ENCLOSURE_SEPARATOR);
					if (enclosureLength != null && enclosureLength.length() > 0) {
						enclosure.append(enclosureLength);
					}
				}
				
				if (enclosure != null && enclosure.length() > 0) {
					enclosureString = enclosure.toString();
					values.put(FeedData.EntryColumns.ENCLOSURE, enclosureString);
					existenceStringBuilder.append(Strings.DB_AND).append(FeedData.EntryColumns.ENCLOSURE).append(Strings.DB_ARG);
				}
				
				String guidString = null;
				
				if (guid != null && guid.length() > 0) {
					guidString = guid.toString();
					values.put(FeedData.EntryColumns.GUID, guidString);
					existenceStringBuilder.append(Strings.DB_AND).append(FeedData.EntryColumns.GUID).append(Strings.DB_ARG);
				}
				
				String entryLinkString = Strings.EMPTY; // don't set this to null as we need *some* value
				
				if (entryLink != null &&  entryLink.length() > 0) {
					entryLinkString = entryLink.toString().trim();
					if (feedBaseUrl != null && !entryLinkString.startsWith(Strings.HTTP) && !entryLinkString.startsWith(Strings.HTTPS)) {
						entryLinkString = feedBaseUrl + (entryLinkString.startsWith(Strings.SLASH) ? entryLinkString : Strings.SLASH + entryLinkString);
					}
				}
				
				String[] existenceValues = enclosureString != null ? (guidString != null ? new String[] {entryLinkString, enclosureString, guidString}: new String[] {entryLinkString, enclosureString}) : (guidString != null ? new String[] {entryLinkString, guidString} : new String[] {entryLinkString});
				
				StringBuilder alternativeExistenceStringBuilder = new StringBuilder(FeedData.EntryColumns.TITLE).append(Strings.DB_ARG);
				String[] alternativeExistenceValues;
				if (entryDate != null) {
					alternativeExistenceStringBuilder.append(Strings.DB_AND).append(FeedData.EntryColumns.DATE).append(Strings.DB_ARG);
					alternativeExistenceValues = new String[] {titleString, Long.toString(entryDate.getTime())};
				} else {
					alternativeExistenceValues = new String[] {titleString};
				}
				
				boolean skip = false;
				
				if ((!efficientFeedParsing || entryDateIsLastUpdateDate || entryIsNewerThanLastUpdate) && (entryDate != null)) {
					StringBuilder existenceWithDateStringBuilder = new StringBuilder(existenceStringBuilder).append(Strings.DB_AND).append(FeedData.EntryColumns.DATE).append("<").append(entryDate.getTime());
					StringBuilder existenceWithDateInclusiveStringBuilder = new StringBuilder(existenceStringBuilder).append(Strings.DB_AND).append(FeedData.EntryColumns.DATE).append("<=").append(entryDate.getTime());
					//do only update an entry if it is sure that it is the one currently handled (in case entries have the same entry link and guid)
					int updated = updateEntryWithAdditionalUniqueness(values, existenceWithDateStringBuilder, existenceWithDateInclusiveStringBuilder, existenceValues, alternativeExistenceStringBuilder, alternativeExistenceValues);
					if (updated > 0) {
						newCount++;
						skip = true;
					} else {
						values.remove(FeedData.EntryColumns.READDATE);
						// continue with the standard procedure but don't reset the read-date
					}
				}
				
				boolean doInsert = false;
				if (!skip) {
					if ((entryLinkString.length() == 0 && guidString == null)) {
						doInsert = true;
					} else {
						//do only update an entry if it is sure that it is the one currently handled (in case entries have the same entry link and guid)
						int updated = updateEntryWithAdditionalUniqueness(values, existenceStringBuilder, null, existenceValues, alternativeExistenceStringBuilder, alternativeExistenceValues);
						if (updated == 0) {
							doInsert = true;
							//if entry has date and wasn't updated check if there is an entry with same title and date (in case entry link (or any other search criteria) has changed)
							if (!efficientFeedParsing && (entryDate != null)) {
								Cursor countCursor = context.getContentResolver().query(feedEntriesUri, new String[] {Strings.DB_COUNT}, alternativeExistenceStringBuilder.toString(), alternativeExistenceValues, null);
								countCursor.moveToFirst();
								int found = countCursor.getInt(0);
								countCursor.close();
								if (found == 1) {
									updated = context.getContentResolver().update(feedEntriesUri, values, alternativeExistenceStringBuilder.toString(), alternativeExistenceValues);
									doInsert = (updated == 0);
								}
							}
						}
					}
				}
				if (doInsert) {                
					values.put(FeedData.EntryColumns.LINK, entryLinkString);
					if (entryDate == null) {
						values.put(FeedData.EntryColumns.DATE, now--);
					}
					
					String entryId = context.getContentResolver().insert(feedEntriesUri, values).getLastPathSegment();
					
					if (fetchImages) {
						FeedDataContentProvider.IMAGEFOLDER_FILE.mkdir(); // create images dir
						for (int n = 0, i = images != null ? images.size() : 0; n < i; n++) {
							try {
								String match = images.get(n);
								
								byte[] data = FetcherService.getBytes(new URL(images.get(n)).openStream());
								
								// see the comment where the img regex is executed for details about this replacement
								FileOutputStream fos = new FileOutputStream(new StringBuilder(FeedDataContentProvider.IMAGEFOLDER).append(entryId).append(Strings.IMAGEFILE_IDSEPARATOR).append(URLEncoder.encode(match.substring(match.lastIndexOf('/')+1), UTF8)).toString().replace(PERCENT, PERCENT_REPLACE));
								
								fos.write(data);
								fos.close();
							} catch (Exception e) {

							}
						}
					}
					
					newCount++;
				} else if (entryDate == null && efficientFeedParsing) {
					cancel();
				}
			} else if (efficientFeedParsing) {
				cancel();
			}
			description = null;
			title = null;
			enclosure = null;
			guid = null;
			author = null;
			enclosureUrl = null;
			enclosureType = null;
			enclosureLength = null;
			entryLink = null;
		} else if (TAG_RSS.equals(localName) || TAG_RDF.equals(localName) || TAG_FEED.equals(localName)) {
			done = true;
		} else if (TAG_GUID.equals(localName)) {
			guidTagEntered = false;
		} else if (TAG_NAME.equals(localName)) {
			nameTagEntered = false;
		} else if (TAG_AUTHOR.equals(localName) || TAG_CREATOR.equals(localName)) {
			authorTagEntered = false;
		} else if (TAG_ENCLOSURE.equals(localName)) {
			enclosureTagEntered = false;
		} else if (TAG_ENCLOSUREURL.equals(localName)) {
			enclosureUrlTagEntered = false;
		} else if (TAG_ENCLOSURETYPE.equals(localName)) {
			enclosureTypeTagEntered = false;
		} else if (TAG_ENCLOSURELENGTH.equals(localName)) {
			enclosureLengthTagEntered = false;
		}
	}
	
	public int getNewCount() {
		return newCount;
	}
	
	public boolean isDone() {
		return done;
	}
	
	public boolean isCancelled() {
		return cancelled;
	}
	
	public void setInputStream(InputStream inputStream) {
		this.inputStream = inputStream;
		reader = null;
	}
	
	public void setReader(Reader reader) {
		this.reader = reader;
		inputStream = null;
	}
	
	public void cancel() {
		if (!cancelled) {
			cancelled = true;
			done = true;
			if (inputStream != null) {
				try {
					inputStream.close(); // stops all parsing
				} catch (IOException e) {
					
				}
			} else if (reader != null) {
				try {
					reader.close(); // stops all parsing
				} catch (IOException e) {
					
				}
			}
		}
	}

	public void setFetchImages(boolean fetchImages) {
		this.fetchImages = fetchImages;
	}
	
	private static Date parseUpdateDate(String string) {
		string = string.replace(Z, GMT);
		for (int n = 0; n < UPDATE_DATEFORMATS.length; n++) {
			try {
				return UPDATE_DATEFORMATS[n].parse(string);
			} catch (ParseException e) { } // just do nothing
		}
		return null;
	}
	
	private static Date parseUpdateDateOnly(String string) {
		for (int n = 0; n < UPDATE_DATEONLYFORMATS.length; n++) {
			try {
				return UPDATE_DATEONLYFORMATS[n].parse(string);
			} catch (ParseException e) { } // just do nothing
		}
		return null;
	}
	
	private static Date parsePubdateDate(String string) {
		for (int n = 0; n < TIMEZONES_COUNT; n++) {
			string = string.replace(TIMEZONES[n], TIMEZONES_REPLACE[n]);
		}
		for (int n = 0; n < PUBDATE_DATEFORMATS.length; n++) {
			try {
				return PUBDATE_DATEFORMATS[n].parse(string);
			} catch (ParseException e) { } // just do nothing
		}
		return null;
	}
	
	private static Date parsePubdateDateOnly(String string) {
		for (int n = 0; n < PUBDATE_DATEONLYFORMATS.length; n++) {
			try {
				return PUBDATE_DATEONLYFORMATS[n].parse(string);
			} catch (ParseException e) { } // just do nothing
		}
		return null;
	}

	private static String unescapeString(String str) {
		String result = str.replace(Strings.AMP_SG, Strings.AMP).replaceAll(Strings.HTML_TAG_REGEX, Strings.EMPTY).replace(Strings.HTML_LT, Strings.LT).replace(Strings.HTML_GT, Strings.GT).replace(Strings.HTML_QUOT, Strings.QUOT).replace(Strings.HTML_APOS, Strings.APOSTROPHE);
		
		Matcher matcher = unicodePattern.matcher(result);
		HashSet<String> unicodesToReplace = new HashSet<String>();
		while (matcher.find()) {
			String toReplace = matcher.group();
			unicodesToReplace.add(toReplace);
		}
		for (String toReplace : unicodesToReplace) {
			int startPos;
			int radix;
			if ((toReplace.charAt(2) == 'x') || (toReplace.charAt(2) == 'X')) {
				startPos = 3;
				radix = 16;
			} else {
				startPos = 2;
				radix = 10;
			}
			String unicodeValue = toReplace.substring(startPos, toReplace.indexOf(";"));
			String character = Character.toString((char) Integer.parseInt(unicodeValue, radix));
			result = result.replace(toReplace, character);
		}
		
		return result;
	}

	public void setEfficientFeedParsing(boolean efficientFeedParsing) {
		this.efficientFeedParsing = efficientFeedParsing;
	}
	
	private int updateEntryWithAdditionalUniqueness(ContentValues values, StringBuilder existenceStringBuilder, StringBuilder existence2StringBuilder, String[] existenceValues, StringBuilder additionalUniquenessStringBuilder, String[] additionalUniquenessValues) {
		//variant 1 (existence2StringBuilder==null): if exactly one entry meets existence criteria update it, if more than one entry is found use additional criteria and update entry (or entries if there is still more than one entry found)
		//variant 2 (existence2StringBuilder!=null): like variant 1 but if a different amount of entries matches existence2 criteria compared to existence criteria do not update them (as it might be the wrong ones that are updated)   
		Cursor countCursor = context.getContentResolver().query(feedEntriesUri, new String[] {Strings.DB_COUNT}, existenceStringBuilder.toString(), existenceValues, null);
		countCursor.moveToFirst();
		int found = countCursor.getInt(0);
		countCursor.close();
		if (found == 0) {
			return 0;
		}
		if (existence2StringBuilder != null) {
			countCursor = context.getContentResolver().query(feedEntriesUri, new String[] {Strings.DB_COUNT}, existence2StringBuilder.toString(), existenceValues, null);
			countCursor.moveToFirst();
			int found2 = countCursor.getInt(0);
			countCursor.close();
			if (found2 > found) {
				return 0;
			}
		}
		if ((found == 1) && !efficientFeedParsing) {
			context.getContentResolver().update(feedEntriesUri, values, existenceStringBuilder.toString(), existenceValues);
			return 1;
		} else {
			StringBuilder enhancedExistenceStringBuilder = new StringBuilder(existenceStringBuilder).append(Strings.DB_AND).append(additionalUniquenessStringBuilder);
			String[] enhancedExistenceValues = new String[existenceValues.length + additionalUniquenessValues.length];
			int index = 0;
			for (String value : existenceValues) {
				enhancedExistenceValues[index] = value;
				++index;
			}
			for (String value : additionalUniquenessValues) {
				enhancedExistenceValues[index] = value;
				++index;
			}
			int updated = context.getContentResolver().update(feedEntriesUri, values, enhancedExistenceStringBuilder.toString(), enhancedExistenceValues);
			return updated;
		}
	}
	
	private void setRealLastUpdateDate(ContentValues values) {
		//do not set date of last update to future dates (which might be accidentally set in the feed or feed entry)
		//as this prevents feed from further updates until the future date is reached (if efficient feed parsing is set)
		//(in some cases it might be better to completely ignore a future date, nevertheless the current implementation helps
		//in most cases where future dates are used)
		if (realLastUpdate > now) {
			realLastUpdate = now;
		}
		values.put(FeedData.FeedColumns.REALLASTUPDATE, realLastUpdate);
	}
}
